-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for creation of associated conversations with message via params #1301
Add support for creation of associated conversations with message via params #1301
Conversation
|> Changeset.cast(attrs, [:user_id]) | ||
|> Changeset.validate_required([:user_id]) | ||
|> Changeset.assoc_constraint(:user) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changeset is needed to cast_assoc
. The message_id
is provided automatically by cast_assoc
and is not part of the params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need a test.
|> Changeset.validate_required([:author_id, :project_id]) | ||
|> Changeset.assoc_constraint(:author) | ||
|> Changeset.assoc_constraint(:project) | ||
|> Changeset.cast_assoc(:conversations, with: &Messages.Conversations.create_changeset/2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create
requires more work than the default changeset
does. We should probably add more tests for the create
function to make sure these changes work. I don't think there's an explicit need to extract a create changeset, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an argument to be made here that this belongs in its own changeset
, akin to Messages.Conversations.create_changeset
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@begedin this is the nested POST, right? I remember doing this once and I had to create a changeset.
I think what I did was take the nested ids, pass it through |> Enum.map(&Changeset.change/1)
and then put_assoc(changeset, :conversations, conversation_changesets)
or something like that. But this looks much better if I am understanding it right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But that required the nested ids to already exist in the DB. Not sure what the case is here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@snewcomer The ids are not in the database in this case. The child record is created alongside the parent.
3d05903
to
6054806
Compare
false -> parsed |> Map.put(type, attributes) | ||
end | ||
end) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The critical part of the included
support is here.
I approached this by following the existing doc for this plug, where it's stated that the goal of the plug is to convert JSON API params into a map of params suitable for Ecto.Changeset
From that, the goal was
- Included
belongs_to
records are merged with params as a submap property, with the key identical to the belongs to record type - Included
has_many
records are merged with params as a list property, with the key being a pluralized version of the record type
This allows us to simply call cast_assoc(changeset, :record_type, opts)
and the casting works out of the box.
I did not work on inferring the pluralized type directly and instead opted to explicitly provide these when calling the plug. It should be sufficient for our needs, but there is a way to infer instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the plug has become quite complex, I think we should add tests for it.
I've fixed existing tests. We still need:
|
|> Changeset.validate_required([:author_id, :project_id]) | ||
|> Changeset.assoc_constraint(:author) | ||
|> Changeset.assoc_constraint(:project) | ||
|> Changeset.cast_assoc(:conversations, with: &Messages.Conversations.create_changeset/2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an argument to be made here that this belongs in its own changeset
, akin to Messages.Conversations.create_changeset
.
records ++ [record] | ||
end) | ||
false -> parsed |> Map.put(type, attributes) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to do a case
here over an if/else
?
|> Changeset.cast(attrs, [:user_id]) | ||
|> Changeset.validate_required([:user_id]) | ||
|> Changeset.assoc_constraint(:user) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need a test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't have the full context of what is going on here. Do the conversations already exist in the DB when posting the message?
|> Changeset.validate_required([:author_id, :project_id]) | ||
|> Changeset.assoc_constraint(:author) | ||
|> Changeset.assoc_constraint(:project) | ||
|> Changeset.cast_assoc(:conversations, with: &Messages.Conversations.create_changeset/2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@begedin this is the nested POST, right? I remember doing this once and I had to create a changeset.
I think what I did was take the nested ids, pass it through |> Enum.map(&Changeset.change/1)
and then put_assoc(changeset, :conversations, conversation_changesets)
or something like that. But this looks much better if I am understanding it right.
|> Changeset.validate_required([:author_id, :project_id]) | ||
|> Changeset.assoc_constraint(:author) | ||
|> Changeset.assoc_constraint(:project) | ||
|> Changeset.cast_assoc(:conversations, with: &Messages.Conversations.create_changeset/2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But that required the nested ids to already exist in the DB. Not sure what the case is here.
@snewcomer the hierarchy is like:
A message could be written with multiple recipients in mind: think of a broadcast message as an example. If we wanted to send an update to all the volunteers, it would be done by creating a message that specifies multiple recipients, all of whom have a conversation created individually for the message. But a message could also be 1:1, for example individually messaging a user, or a user individually messaging a project. In this case there would only be one conversation. |
@joshsmith gotcha. On the frontend, the conversation-part is not serialized up though, correct? At one point does the conversation-part get created? Does that happen completely separate from creating a message? |
@snewcomer yeah, the first conversation part is not the first line of the conversation, which I know is confusing, but necessary due to the |
end) | ||
else | ||
parsed |> Map.put(type, attributes) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to fix this fn because Map.update
's fun parameter only takes a single value (in this case the accumulated records) and we needed to provide the cast_assoc
with an array with the default argument if the key doesn't yet exist (i.e. [attributes]
).
I think this is the logic you were looking for @begedin. Spent quite some time debugging and trying to figure this out. More than I'm happy to admit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the idea is, if there is a many_mapping
key, then the included record is for a has_many
relationship, so it needs to be put into a list.
If there is no many_mapping
key, then the included record is for a belongs_to
relationship, so it needs to be a map.
We don't cast a belongs to this way anywhere yet, so it doesn't matter for now, but that was the intended approach. Had I had the chance to add a test yesterday, this would have been clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, you did the exact thing that was needed. Sorry, this is difficult to follow
Instead of unit testing each individual bit, I opted to integration test on Messages.create/1 and the controller action. The individual bits (changesets) aren't that complex and their behavior is covered with these integration tests. I think this ought to be good to go for now. |
This will need tests fixed for the has_many added to conversations. |
@@ -43,7 +43,7 @@ config :code_corps, CodeCorps.Repo, | |||
pool_size: 10 | |||
|
|||
# CORS allowed origins | |||
config :code_corps, allowed_origins: ["http://localhost:4200"] | |||
config :code_corps, allowed_origins: ["http://localhost:4200", "chrome-extension://pfdhoblngboilpfeibdedpjgfnlcodoo"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs removed.
Will need tests for channels now. Also, I think we should be propagating new conversation parts to the |
I'll add the required tests today, but I'd say we're getting to a point where i'd consider the API side mergeable. It does not kill existing features, so there's little reason for holding off and it would make our life easier. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some questions here.
end | ||
end | ||
def join(_, _, _) do | ||
{:error, %{reason: "unauthenticated"}} | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the Repo.get
returns nil
then we return {:error, %{reason: "unauthenticated"}}
. I think this changes the behavior we had wanted originally, which is determining whether the user is authenticated or not, hence the pattern match on assigns
.
{:ok, %ConversationPart{id: id}} = Messages.add_part(attrs) | ||
assert_broadcast("new:conversation-part", %{id: ^id}) | ||
CodeCorpsWeb.Endpoint.unsubscribe("conversation:#{conversation.id}") | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
import Ecto | ||
import Ecto.Changeset | ||
import CodeCorps.Factories | ||
import CodeCorpsWeb.ChannelCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason this module is being imported into its own using
? I'm guessing it's because it's importing its own namespace into the tests, but is that necessary if the case is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the general pattern for other XCase
modules. Allows us to declare helper methods within this module's namespace, which the test cases can then use.
- Fix DataToAttributes plug's parse_included fn - Add conversation parts to view and controller - Add conversation channel
2c31b58
to
7546651
Compare
What's in this PR?
Update
Still need:
DataToAttributes
plugMessages.create
to check it's possible to create aConversation
alongsideDataToAttributes
andMessages.create
Original text
This PR progress on and hopefully closes #1293
It's been marked as discussion, but due to requiring the set of features described there for client PR code-corps/code-corps-ember#1597, I was forced to start on some type of implementation.
Will comment on specific changes, but to put it in short
ember-data
, by default, does not support including records during post. It has a basic level of support for associating with existing has-many/many-to-many children in the form of the basic{type, id}
identifier map, but not a full include, with all the child record's attributes.ja_serializer does not support it either, so I was forced to provide my own implementation on both ends.
The alternatives are
create conversation manually from client, after creating message - To me, this may actually make sense. It makes the API less opinionated and allows potential other clients later on more flexibility on how they implement their own flow. On the downside, there's no clean way to clean up a message without child conversations if the process is interrupted.
Let the API infer which child records to create - This makes the API extremely opinionated, forces the client down an inflexible route and creates coupling between the two, so I'm against it. We would have to agree on some non-standard way to provide a list of user ids to create conversations for. I guess, we could define a
message has many users through conversations
relationship and do what we do with project skills, project categories, etc, but really, which is one way where I could agree to do it that wayCan't think of any other approach at the moment
References
Progress on: #1293
Tied to: code-corps/code-corps-ember#1597